/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.support;

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.core.Conventions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.AbstractContextLoaderInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FrameworkServlet;

/**
 * Base class for {@link org.springframework.web.WebApplicationInitializer}
 * implementations that register a {@link DispatcherServlet} in the servlet context.
 *
 * <p>Most applications should consider extending the Spring Java config, sub-class
 * {@link AbstractAnnotationConfigDispatcherServletInitializer}.
 *
 * @author Arjen Poutsma
 * @author Chris Beams
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 * @author Stephane Nicoll
 * @since 3.2
 */
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

    /**
     * The default servlet name. Can be customized by overriding {@link #getServletName}.
     */
    public static final String DEFAULT_SERVLET_NAME = "dispatcher";


    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        registerDispatcherServlet(servletContext);
    }

    /**
     * Register a {@link DispatcherServlet} against the given servlet context.
     * <p>This method will create a {@code DispatcherServlet} with the name returned by
     * {@link #getServletName()}, initializing it with the application context returned
     * from {@link #createServletApplicationContext()}, and mapping it to the patterns
     * returned from {@link #getServletMappings()}.
     * <p>Further customization can be achieved by overriding {@link
     * #customizeRegistration(ServletRegistration.Dynamic)} or
     * {@link #createDispatcherServlet(WebApplicationContext)}.
     *
     * @param servletContext the context to register the servlet against
     */
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        Assert.hasLength(servletName, "getServletName() must not return empty or null");

        WebApplicationContext servletAppContext = createServletApplicationContext();
        Assert.notNull(servletAppContext,
                "createServletApplicationContext() did not return an application " +
                        "context for servlet [" + servletName + "]");

        FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
        dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

        ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        Assert.notNull(registration,
                "Failed to register servlet with name '" + servletName + "'." +
                        "Check if there is another servlet registered under the same name.");

        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        Filter[] filters = getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }

        customizeRegistration(registration);
    }

    /**
     * Return the name under which the {@link DispatcherServlet} will be registered.
     * Defaults to {@link #DEFAULT_SERVLET_NAME}.
     *
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected String getServletName() {
        return DEFAULT_SERVLET_NAME;
    }

    /**
     * Create a servlet application context to be provided to the {@code DispatcherServlet}.
     * <p>The returned context is delegated to Spring's
     * {@link DispatcherServlet#DispatcherServlet(WebApplicationContext)}. As such,
     * it typically contains controllers, view resolvers, locale resolvers, and other
     * web-related beans.
     *
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected abstract WebApplicationContext createServletApplicationContext();

    /**
     * Create a {@link DispatcherServlet} (or other kind of {@link FrameworkServlet}-derived
     * dispatcher) with the specified {@link WebApplicationContext}.
     * <p>Note: This allows for any {@link FrameworkServlet} subclass as of 4.2.3.
     * Previously, it insisted on returning a {@link DispatcherServlet} or subclass thereof.
     */
    protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        return new DispatcherServlet(servletAppContext);
    }

    /**
     * Specify application context initializers to be applied to the servlet-specific
     * application context that the {@code DispatcherServlet} is being created with.
     *
     * @see #createServletApplicationContext()
     * @see DispatcherServlet#setContextInitializers
     * @see #getRootApplicationContextInitializers()
     * @since 4.2
     */
    @Nullable
    protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
        return null;
    }

    /**
     * Specify the servlet mapping(s) for the {@code DispatcherServlet} &mdash;
     * for example {@code "/"}, {@code "/app"}, etc.
     *
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected abstract String[] getServletMappings();

    /**
     * Specify filters to add and map to the {@code DispatcherServlet}.
     *
     * @return an array of filters or {@code null}
     * @see #registerServletFilter(ServletContext, Filter)
     */
    @Nullable
    protected Filter[] getServletFilters() {
        return null;
    }

    /**
     * Add the given filter to the ServletContext and map it to the
     * {@code DispatcherServlet} as follows:
     * <ul>
     * <li>a default filter name is chosen based on its concrete type
     * <li>the {@code asyncSupported} flag is set depending on the
     * return value of {@link #isAsyncSupported() asyncSupported}
     * <li>a filter mapping is created with dispatcher types {@code REQUEST},
     * {@code FORWARD}, {@code INCLUDE}, and conditionally {@code ASYNC} depending
     * on the return value of {@link #isAsyncSupported() asyncSupported}
     * </ul>
     * <p>If the above defaults are not suitable or insufficient, override this
     * method and register filters directly with the {@code ServletContext}.
     *
     * @param servletContext the servlet context to register filters with
     * @param filter the filter to be registered
     * @return the filter registration
     */
    protected FilterRegistration.Dynamic registerServletFilter(ServletContext servletContext, Filter filter) {
        String filterName = Conventions.getVariableName(filter);
        Dynamic registration = servletContext.addFilter(filterName, filter);
        if (registration == null) {
            int counter = -1;
            while (counter == -1 || registration == null) {
                counter++;
                registration = servletContext.addFilter(filterName + "#" + counter, filter);
                Assert.isTrue(counter < 100,
                        "Failed to register filter '" + filter + "'." +
                                "Could the same Filter instance have been registered already?");
            }
        }
        registration.setAsyncSupported(isAsyncSupported());
        registration.addMappingForServletNames(getDispatcherTypes(), false, getServletName());
        return registration;
    }

    private EnumSet<DispatcherType> getDispatcherTypes() {
        return (isAsyncSupported() ?
                EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC)
                                   :
                EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
    }

    /**
     * A single place to control the {@code asyncSupported} flag for the
     * {@code DispatcherServlet} and all filters added via {@link #getServletFilters()}.
     * <p>The default value is "true".
     */
    protected boolean isAsyncSupported() {
        return true;
    }

    /**
     * Optionally perform further registration customization once
     * {@link #registerDispatcherServlet(ServletContext)} has completed.
     *
     * @param registration the {@code DispatcherServlet} registration to be customized
     * @see #registerDispatcherServlet(ServletContext)
     */
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    }

}
